iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0
JavaScript

從 JavaScript 到 TypeScript:30 天建立 AI 時代的型別思維系列 第 19 篇

day19|keyof、typeof 與索引存取型別:避免重複維護型別

  • 分享至 

  • xImage
  •  

前一篇用泛型保留不同位置的型別關係。這次介紹如何從既有的值或型別取得資訊,讓鍵名與欄位型別跟著來源更新。

例如題型標籤已經寫在物件裡,卻又另外宣告一份題型聯集:

const questionTypeLabels = {
  choice: "選擇題",
  fill: "填空題",
};

type QuestionType = "choice" | "fill";

新增題型時,物件與聯集都要修改。如果能從物件取得鍵名,就可以只維護一份資料。

typeof 取得既有值的型別

JavaScript 的 typeof 用來判斷值的類型,執行時會回傳 "string"、"object" 等字串。TypeScript 在型別宣告中使用 typeof,則會取得變數或屬性的型別。

兩種寫法可以並排比較:

const questionTypeLabels = {
  choice: "選擇題",
  fill: "填空題",
};

console.log(typeof questionTypeLabels); // "object"

type QuestionTypeLabels = typeof questionTypeLabels;
// { choice: string; fill: string; }

type 名稱 = typeof 變數 讓型別別名參照既有變數的型別,省去另外宣告相同結構的工作。物件欄位仍可改成其他字串,因此 TypeScript 將欄位型別推論為 string。

型別位置的 typeof / Typeof type operator
型別位置的 typeof 可參照變數與屬性,像是 typeof obj.field。其他語法限制可查閱 TypeScript 官方 typeof 文件。

keyof 取得物件型別的鍵名

keyof 型別 會取得該型別允許的鍵名。對於明確列出欄位的物件型別,結果是這些鍵名組成的聯集:

type Question = {
  id: string;
  prompt: string;
  type: "choice" | "fill";
};

type QuestionKey = keyof Question;
// "id" | "prompt" | "type"

let key: QuestionKey = "prompt";
key = "type";
// key = "answer"; // 型別錯誤:不在鍵名聯集中

鍵名聯集可以限制變數只接受已宣告的欄位名稱。修改來源型別的欄位時,允許的鍵名也會跟著更新。

若來源是物件變數,就先用 typeof 取得型別,再用 keyof 取得鍵名:

// 使用前面宣告的 questionTypeLabels
type QuestionType = keyof typeof questionTypeLabels;
// "choice" | "fill"

keyof typeof 變數 可以由右往左讀:先取得變數的型別,再取得該型別的鍵名。這就能取代開頭手寫的題型聯集。

鍵名型別 / Keyof type operator
鍵名也可以是數字或 symbol。若型別允許任意字串或數字作為鍵,keyof 的結果也會包含較廣的型別;相關寫法可查閱 TypeScript 官方 keyof 文件。

questionTypeLabels 物件經由 typeof 轉成型別,再由 keyof 取得 choice 與 fill 鍵名;Question 型別則經由索引存取取得 prompt 的 string 型別。

索引存取型別取得欄位的型別

索引存取型別(indexed access type)的寫法是 型別[鍵名型別],用來取得指定欄位的型別。鍵名可以是單一字面值型別,也可以是聯集;使用聯集時,結果會包含各個欄位的型別:

type QuestionPrompt = Question["prompt"];
// string

type QuestionKind = Question["type"];
// "choice" | "fill"

type QuestionContent = Question["prompt" | "type"];
// string:"choice"、"fill" 已包含在 string 中

Question[keyof Question] 則取得所有欄位型別的聯集。來源欄位的型別改變時,依照這些寫法取得的型別也會更新。

方括號內需要的是型別。若鍵名存在變數中,要先用 typeof 取得它的型別:

const field = "prompt";

// type Prompt = Question[field]; // 型別錯誤:field 是變數名稱
type Prompt = Question[typeof field]; // string

這裡的 const 字串變數推論為 "prompt",所以能作為索引型別。若變數型別是一般的 string,它也可能是不存在的欄位名稱,便無法用來索引這份物件型別。

索引存取型別 / Indexed access type
除了物件欄位,也能用這種寫法取得陣列元素的型別。需要其他用法時,可參考 TypeScript 官方索引存取型別文件。

用 extends 限制可使用的鍵名

前一篇介紹過,型別參數可以用 extends 限定必須符合的型別。搭配 keyof 時,就能要求型別參數只能使用物件型別已有的鍵名:

// keyof Question 是 "id" | "prompt" | "type"
// K 必須符合這個鍵名聯集,才能用來索引 Question
type QuestionField<K extends keyof Question> = Question[K];

// K 指定為 "prompt",取得 Question["prompt"]
type PromptField = QuestionField<"prompt">; // string

// K 指定為 "type",取得 Question["type"]
type Kind = QuestionField<"type">; // "choice" | "fill"

// type Answer = QuestionField<"answer">;
// 型別錯誤:"answer" 不符合 keyof Question 的要求

extends keyof Question 限制 K 可使用的鍵名,Question[K] 則取得對應欄位的型別。這裡用型別別名展示兩者的組合;只需要取得單一欄位型別時,直接寫 Question["prompt"] 即可。

今日練習

day19 | 型旅 TypeTrail

結論

已有物件資料時,可以用 typeof 取得型別,再用 keyof 取得鍵名;已有物件型別時,則能用索引存取型別取得欄位型別。這些寫法讓相關宣告共用同一份來源,減少資料改動時需要同步修改的地方。

有時候我們只需要既有型別中的幾個欄位,或想把必填欄位改成選填。下一篇會看看工具型別如何省去重新宣告的工作。


本文同步分享於個人部落格:https://johnsonchen.dev/blog/typescript/type-operators/


上一篇
day18|泛型:保留輸入與輸出的關係
下一篇
day20|工具型別:從既有型別產生新型別
系列文
從 JavaScript 到 TypeScript:30 天建立 AI 時代的型別思維 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言